import express from 'express';
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import path from 'path';
import fs from 'fs/promises';
import { JSDOM } from 'jsdom';
import { Readability } from '@mozilla/readability';
import TurndownService from 'turndown';
import { Browser, Page } from 'puppeteer';

// Constants
const MAX_BROWSER_INSTANCES = 5;
const MIN_BROWSER_INSTANCES = 2;
const REQUEST_TIMEOUT = 30000;

// Browser pool interface
interface BrowserPool {
  acquire(): Promise<Browser>;
  release(browser: Browser): Promise<void>;
  drain(): Promise<void>;
  clear(): Promise<void>;
}

// Simple browser pool implementation
class SimpleBrowserPool implements BrowserPool {
  private available: Browser[] = [];
  private inUse: Set<Browser> = new Set();
  private creating: Promise<void> | null = null;

  constructor(
    private readonly min: number,
    private readonly max: number
  ) {
    this.initialize();
  }

  private async initialize() {
    this.creating = this.ensureMinimum();
  }

  private async ensureMinimum(): Promise<void> {
    while (this.available.length + this.inUse.size < this.min) {
      const browser = await this.createBrowser();
      this.available.push(browser);
    }
  }

  private async createBrowser(): Promise<Browser> {
    return await puppeteer.launch({
      headless: "new",
      args: [
        '--no-sandbox',
        '--disable-setuid-sandbox',
        '--disable-dev-shm-usage',
        '--disable-gpu',
        '--disable-web-security',
        '--disable-features=IsolateOrigins,site-per-process',
        '--disable-blink-features=AutomationControlled'
      ]
    });
  }

  async acquire(): Promise<Browser> {
    if (this.creating) {
      await this.creating;
    }

    if (this.available.length === 0) {
      if (this.inUse.size < this.max) {
        const browser = await this.createBrowser();
        this.inUse.add(browser);
        return browser;
      }
      throw new Error('No browsers available and pool is at maximum capacity');
    }

    const browser = this.available.pop()!;
    this.inUse.add(browser);
    return browser;
  }

  async release(browser: Browser): Promise<void> {
    if (this.inUse.has(browser)) {
      this.inUse.delete(browser);
      if (this.available.length + this.inUse.size < this.min) {
        this.available.push(browser);
      } else {
        await browser.close();
      }
    }
  }

  async drain(): Promise<void> {
    await Promise.all([
      ...this.available.map(browser => browser.close()),
      ...Array.from(this.inUse).map(browser => browser.close())
    ]);
    this.available = [];
    this.inUse.clear();
  }

  async clear(): Promise<void> {
    await Promise.all(this.available.map(browser => browser.close()));
    this.available = [];
  }

  getStats() {
    return {
      available: this.available.length,
      inUse: this.inUse.size,
      total: this.available.length + this.inUse.size
    };
  }
}

// Create browser pool
const browserPool = new SimpleBrowserPool(MIN_BROWSER_INSTANCES, MAX_BROWSER_INSTANCES);

// Monitor pool stats
setInterval(() => {
  const stats = browserPool.getStats();
  console.log('Browser pool stats:', stats);
}, 60000);

// Safe file writing
async function safeWriteFile(filePath: string, content: string | Buffer): Promise<void> {
  const tempPath = `${filePath}.tmp`;
  try {
    await fs.writeFile(tempPath, content);
    await fs.rename(tempPath, filePath);
  } catch (err) {
    try {
      await fs.unlink(tempPath);
    } catch (unlinkErr) {
      // Ignore cleanup errors
    }
    throw err;
  }
}

// Create JSDOM window for HTML element checks
const { window } = new JSDOM();

// Markdown cleaning utility
function tidyMarkdown(markdown: string): string {
  // Handle complex broken links with text and optional images
  let normalizedMarkdown = markdown.replace(/\[\s*([^\]\n]+?)\s*\]\s*\(\s*([^)]+)\s*\)/g, (match, text, url) => {
    text = text.replace(/\s+/g, ' ').trim();
    url = url.replace(/\s+/g, '').trim();
    return `[${text}](${url})`;
  });

  // Handle links with images
  normalizedMarkdown = normalizedMarkdown.replace(/\[\s*([^\]\n!]*?)\s*\n*(?:!\[([^\]]*)\]\((.*?)\))?\s*\n*\]\s*\(\s*([^)]+)\s*\)/g, (match, text, alt, imgUrl, linkUrl) => {
    text = text.replace(/\s+/g, ' ').trim();
    alt = alt ? alt.replace(/\s+/g, ' ').trim() : '';
    imgUrl = imgUrl ? imgUrl.replace(/\s+/g, '').trim() : '';
    linkUrl = linkUrl.replace(/\s+/g, '').trim();
    return imgUrl ? `[${text} ![${alt}](${imgUrl})](${linkUrl})` : `[${text}](${linkUrl})`;
  });

  // Replace multiple empty lines with exactly two
  normalizedMarkdown = normalizedMarkdown.replace(/\n{3,}/g, '\n\n');

  // Remove leading spaces from each line
  normalizedMarkdown = normalizedMarkdown.replace(/^[ \t]+/gm, '');

  return normalizedMarkdown.trim();
}

const app = express();
app.use(express.json());

// Initialize Turndown with custom rules
const turndownService = new TurndownService({
  headingStyle: 'atx',
  codeBlockStyle: 'fenced',
  hr: '---',
  bulletListMarker: '-',
  emDelimiter: '_',
  strongDelimiter: '**'
});

// Add custom rules
turndownService.addRule('improved-code', {
  filter: (node: Node): boolean => {
    if (node.nodeName !== 'CODE') return false;
    const parent = node.parentNode;
    if (!parent) return false;
    const hasSiblings = node.previousSibling || node.nextSibling;
    const isCodeBlock = parent.nodeName === 'PRE' && !hasSiblings;
    return !isCodeBlock;
  },
  replacement: (content: string, node: Node, options: TurndownService.Options): string => {
    if (!content) return '';
    let delimiter = '`';
    const matches = content.match(/`+/gm);
    if (matches) {
      while (matches.includes(delimiter)) {
        delimiter = delimiter + '`';
      }
    }
    if (content.includes('\n')) {
      delimiter = options.codeBlockStyle === 'fenced' ? '```' : '~~~';
    }
    const extraSpace = delimiter.length > 1 ? '\n' : /^`|^ .*?[^ ].* $|`$/.test(content) ? ' ' : '';
    return delimiter + extraSpace + content + extraSpace + delimiter;
  }
});

turndownService.addRule('improved-paragraph', {
  filter: ['p'],
  replacement: (content: string): string => {
    const trimmed = content.trim();
    if (!trimmed) return '';
    return `${trimmed.replace(/\n{3,}/g, '\n\n')}\n\n`;
  }
});

turndownService.addRule('improved-inline-link', {
  filter: (node: Node): boolean => {
    return node.nodeName === 'A' && node instanceof window.HTMLElement && !!node.getAttribute('href');
  },
  replacement: (content: string, node: Node): string => {
    if (!(node instanceof window.HTMLElement)) return content;
    const href = node.getAttribute('href') || '';
    const title = node.getAttribute('title');
    const titlePart = title ? ` "${title.replace(/"/g, '\\"')}"` : '';
    const fixedContent = content.replace(/\s+/g, ' ').trim();
    const fixedHref = href.replace(/\s+/g, '').trim();
    return `[${fixedContent}](${fixedHref}${titlePart})`;
  }
});

turndownService.addRule('improved-image', {
  filter: ['img'],
  replacement: (content: string, node: Node): string => {
    if (!(node instanceof window.HTMLElement)) return content;
    const alt = node.getAttribute('alt') || '';
    const src = node.getAttribute('src') || '';
    const title = node.getAttribute('title') || '';
    const titlePart = title ? ` "${title}"` : '';
    return `![${alt}](${src}${titlePart})`;
  }
});

// Create storage directory if it doesn't exist
const storagePath = path.join(__dirname, '../storage');
fs.mkdir(storagePath, { recursive: true }).catch(console.error);

// Configure puppeteer
puppeteer.use(StealthPlugin());

// Routes
app.post('/api/crawl', async (req, res) => {
  let browser: Browser | null = null;
  let page: Page | null = null;

  try {
    const { url } = req.body;
    const isZhihu = url.includes('zhihu.com');

    console.log(`Crawling URL: ${url}`);

    // Create timeout promise
    const timeoutPromise = new Promise((_, reject) => {
      setTimeout(() => reject(new Error('Request timeout')), REQUEST_TIMEOUT);
    });

    // Acquire browser from pool
    browser = await browserPool.acquire();
    page = await browser.newPage();

    // Set up page configuration
    if (isZhihu) {
      const zhihuUA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36';
      await page.setUserAgent(zhihuUA);

      await page.evaluateOnNewDocument(() => {
        Object.defineProperty(navigator, 'webdriver', { get: () => undefined });
        Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });
        Object.defineProperty(navigator, 'languages', { get: () => ['zh-CN', 'zh'] });
        localStorage.setItem('_zap', 'some-random-value');
      });

      await page.setExtraHTTPHeaders({
        'Accept-Language': 'zh-CN,zh;q=0.9',
        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
        'Accept-Encoding': 'gzip, deflate, br',
        'Connection': 'keep-alive',
        'sec-ch-ua': '"Not_A Brand";v="8", "Chromium";v="120", "Google Chrome";v="120"',
        'sec-ch-ua-mobile': '?0',
        'sec-ch-ua-platform': '"macOS"',
        'Sec-Fetch-Dest': 'document',
        'Sec-Fetch-Mode': 'navigate',
        'Sec-Fetch-Site': 'none',
        'Sec-Fetch-User': '?1',
        'Upgrade-Insecure-Requests': '1'
      });

      await page.waitForTimeout(Math.floor(Math.random() * 2000) + 1000);
    }

    // Navigate with retry logic
    let retryCount = 0;
    const maxRetries = 3;
    let navigationError: Error | null = null;

    while (retryCount < maxRetries) {
      try {
        await Promise.race([
          page.goto(url, {
            waitUntil: 'networkidle0',
            timeout: 30000
          }),
          timeoutPromise
        ]);

        if (isZhihu) {
          await page.waitForSelector('.Question-main', { timeout: 10000 }).catch(() => {});
          await page.waitForTimeout(2000);
        }

        navigationError = null;
        break;
      } catch (navError: any) {
        navigationError = navError;
        retryCount++;
        if (retryCount < maxRetries) {
          console.log(`Retry attempt ${retryCount} for ${url}`);
          await page.waitForTimeout(2000 * retryCount);
        }
      }
    }

    if (navigationError) {
      throw new Error(`Failed to navigate to ${url} after ${maxRetries} attempts: ${navigationError.message}`);
    }

    // Handle Zhihu login modal
    if (isZhihu) {
      const loginModalSelector = '.Modal-wrapper';
      const hasLoginModal = await page.$(loginModalSelector);
      if (hasLoginModal) {
        await page.evaluate((selector) => {
          const modal = document.querySelector(selector);
          if (modal) modal.remove();
          const masks = document.querySelectorAll('.Modal-backdrop');
          masks.forEach(mask => mask.remove());
        }, loginModalSelector);
      }
    }

    // Get page content
    const content = await page.content();
    const title = await page.title();

    // Extract content using Readability
    const dom = new JSDOM(content);
    const reader = new Readability(dom.window.document);
    const article = reader.parse();

    // Convert to Markdown
    const markdown = article ? tidyMarkdown(turndownService.turndown(article.content)) : '';

    // Extract images
    const images = await page.evaluate(() => {
      return Array.from(document.images).map(img => ({
        src: img.src,
        alt: img.alt || '',
        width: img.width,
        height: img.height
      }));
    });

    // Save files
    const timestamp = Date.now();
    const fileName = `${timestamp}-${new URL(url).hostname}.html`;
    const filePath = path.join(storagePath, fileName);
    await safeWriteFile(filePath, content);

    const extractedContent = {
      title: article?.title || title,
      excerpt: article?.excerpt || '',
      markdown,
      images,
      url,
      savedAs: fileName,
      timestamp
    };

    const extractedFilePath = path.join(storagePath, `${timestamp}-${new URL(url).hostname}.json`);
    await safeWriteFile(extractedFilePath, JSON.stringify(extractedContent, null, 2));

    res.json(extractedContent);
  } catch (err) {
    const error = err as Error;
    console.error('Crawl error:', error);
    res.status(500).json({ error: error.message });
  } finally {
    if (page) {
      await page.close().catch(console.error);
    }
    if (browser) {
      await browserPool.release(browser).catch(console.error);
    }
  }
});

// Graceful shutdown
process.on('SIGTERM', async () => {
  console.log('SIGTERM received. Shutting down gracefully...');
  await browserPool.drain();
  process.exit(0);
});

process.on('SIGINT', async () => {
  console.log('SIGINT received. Shutting down gracefully...');
  await browserPool.drain();
  process.exit(0);
});

// Start server
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});
